package org.tio.sitexxx.web.server.http;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tio.http.common.Cookie;
import org.tio.http.common.HeaderName;
import org.tio.http.common.HeaderValue;
import org.tio.http.common.HttpRequest;
import org.tio.http.common.HttpResponse;
import org.tio.http.common.HttpResponseStatus;
import org.tio.http.common.MimeType;
import org.tio.http.common.RequestLine;
import org.tio.http.common.session.HttpSession;
import org.tio.http.common.utils.HttpGzipUtils;
import org.tio.http.server.intf.HttpServerInterceptor;
import org.tio.http.server.util.Resps;
import org.tio.sitexxx.service.cache.CacheConfig;
import org.tio.sitexxx.service.cache.Caches;
import org.tio.sitexxx.service.model.conf.Httpcache;
import org.tio.sitexxx.service.model.main.IpInfo;
import org.tio.sitexxx.service.model.main.LoginLog;
import org.tio.sitexxx.service.model.main.User;
import org.tio.sitexxx.service.model.main.UserAgent;
import org.tio.sitexxx.service.model.stat.TioSlowRequest;
import org.tio.sitexxx.service.service.base.IpInfoService;
import org.tio.sitexxx.service.service.base.UserAgentService;
import org.tio.sitexxx.service.service.conf.ConfService;
import org.tio.sitexxx.service.service.conf.HttpcacheService;
import org.tio.sitexxx.service.service.conf.IpWhiteListService;
import org.tio.sitexxx.service.vo.AppCode;
import org.tio.sitexxx.service.vo.Const;
import org.tio.sitexxx.service.vo.RequestExt;
import org.tio.sitexxx.service.vo.RequestKey;
import org.tio.sitexxx.service.vo.SessionExt;
import org.tio.sitexxx.service.vo.SessionKey;
import org.tio.sitexxx.web.server.auth.AccessCtrlConfig;
import org.tio.sitexxx.web.server.auth.AccessCtrlService;
import org.tio.sitexxx.web.server.utils.WebUtils;
import org.tio.utils.SystemTimer;
import org.tio.utils.cache.ICache;
import org.tio.utils.jfinal.P;
import org.tio.utils.json.Json;
import org.tio.utils.lock.LockUtils;
import org.tio.utils.resp.Resp;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;

/**
 * @author tanyaowu
 * 2016年8月3日 下午1:27:05
 */
public class TioSiteHttpServerInterceptor implements HttpServerInterceptor {
	private static Logger log = LoggerFactory.getLogger(TioSiteHttpServerInterceptor.class);

	public static final TioSiteHttpServerInterceptor ME = new TioSiteHttpServerInterceptor();

	private final String httpCacheLockKey = this.getClass().getName() + ".httpCacheLockKey";

	private static final HeaderName HTTPCACHE_FLAG_HEADER_NAME = HeaderName.from("tio-httpcache-old");

	private static final HeaderName HTTPCACHE_FIRST_HEADER_NAME = HeaderName.from("tio-httpcache-new");

	private static final HeaderName HEADER_NAME_WEBAPI_SERVER = HeaderName.from("tio-webapi-server");

	private static final HeaderValue HEADER_VALUE_WHICH_API = HeaderValue.from(Const.SERVICE_HOST);
	
	/**
	 * APP的每个请求都要传这个参数: tio_site_from_app=1
	 */
	private static final String PARAM_NAME_IS_FROM_APP = "tio_site_from_app";

	private static final Object lockForGetLock = new Object();

	private AccessCtrlConfig accessCtrlConfig;

	/**
	 * @param args
	 * @author tanyaowu
	 */
	public static void main(String[] args) {

	}

	/**
	 *
	 * @author tanyaowu
	 */
	private TioSiteHttpServerInterceptor() {
	}

	private static byte[] BODY_BYTES_NEED_ACCESS_TOKEN = null;

	/**
	 * 不用进行access token检查的path
	 */
	private static Set<String> skipCheckAccessTokenPathSet = new HashSet<>();

	/**
	 * 必须进行access token检查的path（优先级最高 ）
	 */
	private static Set<String> neededCheckAccessTokenPathSet = new HashSet<>();

	/**
	 * 不用进行access token检查的path的前缀
	 */
	private static String[] skipCheckAccessTokenPathprefix = new String[] {};

	static {
		//不需要验证
		skipCheckAccessTokenPathSet.add("/a/x");
		skipCheckAccessTokenPathSet.add("/a/y");
		skipCheckAccessTokenPathSet.add("/wechat/service");

		//博客上传图片，因为是没有走自己的ajax请求
		skipCheckAccessTokenPathSet.add("/blog/uploadimg");
		skipCheckAccessTokenPathSet.add("/ad/redirect");

		// 博客保存，编辑文章，时间可能会过长，防止误刷新
		skipCheckAccessTokenPathSet.add("/blog/save");

		Resp resp = Resp.fail().code(AppCode.ForbidOper.NEED_ACCESS_TOKEN);
		String xx = Json.toJson(resp);
		try {
			BODY_BYTES_NEED_ACCESS_TOKEN = xx.getBytes(org.tio.http.common.HttpConst.CHARSET_NAME);
		} catch (UnsupportedEncodingException e) {
			log.error(e.toString(), e);
		}
	}

	/**
	 * 判断是否需要进行accesstoken检查
	 * @param request
	 * @param path
	 * @return true: 需要进行accesstoken检查
	 */
	private static boolean needCheckAccessToken(HttpRequest request, String path) {
		if (neededCheckAccessTokenPathSet.contains(path)) {
			return true;
		}

		if (skipCheckAccessTokenPathSet.contains(path)) {
			return false;
		}
		for (String pathPrefix : skipCheckAccessTokenPathprefix) {
			if (StrUtil.startWith(path, pathPrefix)) {
				return false;
			}
		}

		return true;
	}

	@Override
	public HttpResponse doBeforeHandler(HttpRequest request, RequestLine requestLine, HttpResponse httpResponseFromCache) throws Exception {
		RequestExt requestExt = new RequestExt();
		request.setAttribute(RequestKey.REQUEST_EXT, requestExt);
		requestExt.setCanCache(false);

		String path = requestLine.getPath(); //这个是去头和去尾的，形如: /video/videoList

		int accessTokenOn = P.getInt("use.access.token", 2);

		IpInfo ipInfo = IpInfoService.ME.save(request.getClientIp());
		requestExt.setIpInfo(ipInfo);

		//从浏览器（Browser）过来的
		String userAgentStr = request.getUserAgent();
		UserAgent userAgent = UserAgentService.ME.save(userAgentStr);
		requestExt.setUserAgent(userAgent);

		String isFromApp = request.getParam(PARAM_NAME_IS_FROM_APP);
		if (StrUtil.isNotBlank(isFromApp) && "1".equals(isFromApp)) {  //app过来的请求
			requestExt.setFromBrowser(false);
			requestExt.setFromBrowserPc(false);
			requestExt.setFromBrowserMobile(false);
		} else {
			boolean isMobile = UserAgent.isMobile(userAgent);
			requestExt.setFromBrowser(true);
			requestExt.setFromBrowserPc(!isMobile); //不是移动浏览器就是PC浏览器
			requestExt.setFromBrowserMobile(isMobile);
		}
		
		//白名单权限控制
		boolean isWhiteIp = IpWhiteListService.isWhiteIp(request.getClientIp());

		//看看access_token start
		if (!request.isForward()) {
			if (!isWhiteIp && accessTokenOn == 1) {
				if (needCheckAccessToken(request, path)) {
					//检查tio_access_token
					Cookie cookie = request.getCookie(Const.AccessToken.COOKIENAME_FOR_ACCESSTOKEN);
					boolean needNewAccessToken = true;
					if (cookie == null) {

					} else {
						String value = cookie.getValue();
						if (StrUtil.isBlank(value)) {

						} else {
							ICache cache2 = Caches.getCache(CacheConfig.TIO_ACCESS_TOKEN);
							String valueInCache = cache2.get(request.getHttpSession().getId(), String.class);

							if (Objects.equals(value, valueInCache)) {
								needNewAccessToken = false;
							}
						}
					}

					if (needNewAccessToken) {
						HttpResponse ret = Resps.bytesWithContentType(request, BODY_BYTES_NEED_ACCESS_TOKEN, MimeType.TEXT_PLAIN_JSON.getType());
						return ret;
					}
				}
			}
		}
		//看看access_token end

		HttpSession session = request.getHttpSession();

		User user = WebUtils.currUser(request);
		Integer userid = null;
		if (user != null) {
			userid = user.getId();
		}

		boolean b = AccessCtrlService.canAccess(accessCtrlConfig, userid, path);
		if (!b) //没有权限
		{
			if (user != null) {
				Resp resp = Resp.fail("没权限访问").code(AppCode.ForbidOper.NOTPERMISSION);
				return Resps.json(request, resp);
			} else {
				SessionExt sessionExt = session.getAttribute(SessionKey.SESSION_EXT, SessionExt.class);
				LoginLog loginLog = sessionExt.getKickedInfo();
				if (loginLog != null) {
					String ip = loginLog.getIp();
					Date time = loginLog.getTime();
					String msg = "异地登录，您的帐号于" + DateUtil.formatDateTime(time) + "在" + ip + "登录过";

					Resp resp = Resp.fail(msg).code(AppCode.ForbidOper.KICKTED);
					return Resps.json(request, resp);
				} else {
					Resp resp = Resp.fail("您尚未登录或登录超时").code(AppCode.ForbidOper.NOTLOGIN);
					return Resps.json(request, resp);
				}
			}
		}

		requestExt.setCanCache(true);

		HttpResponse httpResponse = doHttpCacheOnBeforeHandler(request, requestExt, path, httpCacheLockKey, useHttpcache);
		return httpResponse;
	}

	public static HttpResponse doHttpCacheOnBeforeHandler(HttpRequest request, RequestExt requestExt, String path, String httpCacheLockKey, boolean useHttpcache) throws Exception {
		ICache cache = null;
		if (useHttpcache) {
			cache = HttpcacheService.getCache(path);
		}
		//在表中配了http缓存
		if (useHttpcache && cache != null) {
			Httpcache httpcache = HttpcacheService.get(path);
			if (httpcache != null) {
				String cacheKey = getHttpcacheKey(request, cache, httpcache);
				HttpResponse httpResponse = cache.get(cacheKey, HttpResponse.class);//.put(cacheKey, value);
				if (httpResponse != null) {
					//					System.out.println("use httpcache " + path + ", key: " + cacheKey);
					return cloneAnd304(request, requestExt, httpResponse);
				} else {
					ReentrantReadWriteLock lock = LockUtils.getReentrantReadWriteLock(cacheKey, lockForGetLock);//(cacheKey);
					WriteLock writeLock = lock.writeLock();
					boolean tryWrite = writeLock.tryLock();
					if (tryWrite) {
						request.setAttribute(httpCacheLockKey, writeLock);
						httpResponse = cache.get(cacheKey, HttpResponse.class);
						if (httpResponse != null) {
							return cloneAnd304(request, requestExt, httpResponse);
						}
						return null;
					} else {
						ReadLock readLock = lock.readLock();
						boolean tryRead = readLock.tryLock(10, TimeUnit.SECONDS);
						if (tryRead) {
							request.setAttribute(httpCacheLockKey, readLock);
							httpResponse = cache.get(cacheKey, HttpResponse.class);
							if (httpResponse != null) {
								return cloneAnd304(request, requestExt, httpResponse);
							}
							return null;
						} else {
							return null;
						}
					}
				}
			}
		}

		return null;
	}

	//	String writeLockKey = this.getClass().getName() + ".writeLockKey";

	private static HttpResponse cloneAnd304(HttpRequest request, RequestExt requestExt, HttpResponse httpResponse) {
		HttpResponse clone = HttpResponse.cloneResponse(request, httpResponse);
		requestExt.setFromCache(true);

		HeaderValue lastModified = clone.getLastModified();
		if (lastModified != null) {
			try {
				long _lastModified = Long.parseLong(lastModified.value);
				HttpResponse r304 = Resps.try304(request, _lastModified);
				if (r304 != null) {
					r304.addHeader(HTTPCACHE_FLAG_HEADER_NAME, clone.getHeader(HTTPCACHE_FLAG_HEADER_NAME));
					return r304;
				}
			} catch (NumberFormatException e) {
				//							log.error(e.toString(), e);
				return clone;
			}
		}

		return clone;
	}

	//	private Map<String, ReentrantReadWriteLock> lockMap = new HashMap<>();

	//	/**
	//	 * key  : 形如：name=tan&id=123的cachekey
	//	 * value: ReentrantReadWriteLock
	//	 */
	//	private final MapWithLock<String, ReentrantReadWriteLock> lockMap = new MapWithLock<>(new HashMap<String, ReentrantReadWriteLock>());
	//
	//	private ReentrantReadWriteLock getLock(String cacheKey) {
	//		ReentrantReadWriteLock lock = lockMap.get(cacheKey);
	//		if (lock != null) {
	//			return lock;
	//		}
	//
	//		lock = lockMap.putIfAbsent(cacheKey, new ReentrantReadWriteLock());
	//		return lock;
	//
	//		//		synchronized (lockMap) {
	//		//			lock = lockMap.get(cacheKey);
	//		//			if (lock != null) {
	//		//				return lock;
	//		//			}
	//		//			lock = new ReentrantReadWriteLock();
	//		//			lockMap.put(cacheKey, lock);
	//		//			return lock;
	//		//		}
	//	}

	@Override
	public void doAfterHandler(HttpRequest request, RequestLine requestLine, HttpResponse response, long cost) throws Exception {
		RequestExt requestExt = WebUtils.getRequestExt(request);
		try {

		} catch (Exception e) {
			log.error(e.toString(), e);
		} finally {
			doHttpCacheOnAfterHandler(response, request, requestExt, requestLine.path, useHttpcache, httpCacheLockKey);
			TioSiteHttpServerInterceptor.saveSlowRequest(request, requestLine, response, cost, (byte) 1);
		}
	}

	public static HttpResponse doHttpCacheOnAfterHandler(HttpResponse response, HttpRequest request, RequestExt requestExt, String path, boolean useHttpcache,
	        String httpCacheLockKey) {
		String cacheKey = null;
		try {

			if (requestExt.isFromCache()) {
				return response;
			}

			ICache cache = null;//HttpcacheService.getCache(path);
			//		boolean usehttpcache = P.getInt("web.api.use.http.cache", 1) == 1;
			if (useHttpcache) {
				cache = HttpcacheService.getCache(path);
			}
			//在表中配了http缓存
			if (useHttpcache && cache != null) {
				Httpcache httpcache = HttpcacheService.get(path);
				if (httpcache != null) {
					if (response != null) {
						if (response.getStatus() == HttpResponseStatus.C200 && requestExt.isCanCache()) {
							cacheKey = getHttpcacheKey(request, cache, httpcache);
							HeaderValue headerValueCacheKey = HeaderValue.from(cacheKey);
							HeaderValue lastModified = HeaderValue.from(SystemTimer.currTime + "");

							response.setLastModified(lastModified);
							HttpGzipUtils.gzip(request, response);

							HttpResponse responseForCache = HttpResponse.cloneResponse(request, response);
							responseForCache.addHeader(HTTPCACHE_FLAG_HEADER_NAME, headerValueCacheKey);
							cache.put(cacheKey, responseForCache);

							response.addHeader(HTTPCACHE_FIRST_HEADER_NAME, headerValueCacheKey);

							response.addHeader(HEADER_NAME_WEBAPI_SERVER, HEADER_VALUE_WHICH_API);

							return response;
						}
					}
				}
			}
		} catch (Exception e) {
			log.error(e.toString(), e);
		} finally {
			try {
				Lock lock = (Lock) request.getAttribute(httpCacheLockKey);
				if (lock != null) {
					if (path == null) {
						path = request.requestLine.getPath();
					}
					log.info("httpcache释放锁【{}】, 这是正常日志. path:【{}】, cacheKey:{}", lock.getClass().getName(), path, cacheKey);
					lock.unlock();
				}
			} catch (Exception e) {
				log.error(request.requestLine.toString(), e);
			}
		}

		return response;
	}

	private static boolean useHttpcache = P.getInt("web.api.use.http.cache", 1) == 1;

	public static void saveSlowRequest(HttpRequest request, RequestLine requestLine, HttpResponse response, long cost, byte type) {
		int slow_request_cost = ConfService.getInt("slow_request_cost", 2000);
		if (cost >= slow_request_cost) {
			try {
				Date endtime = new Date();
				Date starttime = new Date(endtime.getTime() - cost);

				Integer uid = WebUtils.currUserId(request);

				TioSlowRequest tioSlowRequest = new TioSlowRequest();
				tioSlowRequest.setType(type);
				tioSlowRequest.setCost(cost);
				tioSlowRequest.setPath(requestLine.getPathAndQuery());
				tioSlowRequest.setBody(request.getBodyString());
				tioSlowRequest.setEndtime(endtime);
				tioSlowRequest.setStarttime(starttime);
				tioSlowRequest.setUid(uid);
				tioSlowRequest.setSession(request.getHttpSession().getId());

				tioSlowRequest.save();
			} catch (Exception e) {
				log.error(e.toString(), e);
			}
		}
	}

	/**
	 * 
	 * @param request
	 * @param cache
	 * @param httpcache
	 * @return
	 */
	private static String getHttpcacheKey(HttpRequest request, ICache cache, Httpcache httpcache) {
		Integer currUid = WebUtils.currUserId(request);
		Map<String, Object> params = null;
		String[] paramArray = httpcache.getParamArray();
		if (paramArray != null && paramArray.length > 0) {
			params = new HashMap<>();
			for (String name : paramArray) {
				String value = request.getParam(name);
				params.put(name, value);
			}
		}

		return getHttpcacheKey(currUid, params, cache, httpcache);
	}

	public static String getHttpcacheKey(Integer currUid, Map<String, Object> params, ICache cache, Httpcache httpcache) {
		String[] paramArray = httpcache.getParamArray();
		//		String[] cookieArray = httpcache.getCookieArray();
		boolean isUseUidAsKey = httpcache.isUseUidAsKey(); //是否使用userid作为cachekey
		boolean isUseLoginedAsKey = httpcache.isUseLoginedAsKey(); //是否使用登录状态作为cachekey

		StringBuilder key = new StringBuilder(30);
		if (isUseUidAsKey && currUid != null) {
			key.append("u{").append(currUid).append("}");
		}

		if (isUseLoginedAsKey) {
			if (currUid != null) {
				key.append("l{1}");
			} else {
				key.append("l{0}");
			}
		}

		if (paramArray != null && params != null) {
			key.append("p{");
			for (String name : paramArray) {
				Object value = params.get(name);
				if (value != null) {
					key.append(name).append("=").append(value).append("&");
				}
			}
			key.append("}");
		}

		//		if (cookieArray != null) {
		//			key.append("c{");
		//			for (String name : cookieArray) {
		//				Cookie cookie = request.getCookie(name);
		//				if (cookie != null) {
		//					key.append(name).append("=").append(cookie.getValue()).append("&");
		//				}
		//			}
		//			key.append("}");
		//		}

		if (key.length() == 0) {
			return "t-io";
		} else {
			return key.toString();
		}
	}

	/**
	 * 
	 * @param path
	 * @param params
	 * @param userid
	 */
	public static void clearHttpcache(String path, Map<String, Object> params, Integer userid) {
		ICache cache = HttpcacheService.getCache(path);
		if (cache != null) {
			Httpcache httpcache = HttpcacheService.get(path);
			if (httpcache != null) {
				String cacheKey = getHttpcacheKey(userid, params, cache, httpcache);
				cache.remove(cacheKey);
			}
		}
	}

	public AccessCtrlConfig getAccessCtrlConfig() {
		return accessCtrlConfig;
	}

	public void setAccessCtrlConfig(AccessCtrlConfig accessCtrlConfig) {
		this.accessCtrlConfig = accessCtrlConfig;
	}
}
